package xpath

import (
	"bytes"
	"strings"

	"gopkg.in/xmlpath.v2"

	"code.google.com/awwwe-browser/data"
)

type boolTestType int

const (
	_ boolTestType = iota
	booleanTestType
	logicalTestType
)

type logicalType int

const (
	_ logicalType = iota
	andLogicalType
	orLogicalType
	nilLogicalType
)

type booleanTest struct {
	testType  boolTestType
	truefalse bool
	andor     logicalType
}

func buildBooleanTestTypeAnd() booleanTest {
	var v booleanTest
	v.testType = logicalTestType
	v.truefalse = false
	v.andor = andLogicalType
	return v
}

func buildBooleanTestTypeOr() booleanTest {
	var v booleanTest
	v.testType = logicalTestType
	v.truefalse = false
	v.andor = orLogicalType
	return v
}

func buildBooleanTestType(truefalse bool) booleanTest {
	var v booleanTest
	v.testType = booleanTestType
	v.truefalse = truefalse
	v.andor = nilLogicalType
	return v
}

// returns if the expression is true
// context is the xmlpath.Node on which to apply the expression.
func IsTrueString(expression string, context *xmlpath.Node) bool {
	var bfr *bytes.Buffer
	var bb []byte
	bfr = bytes.NewBuffer(data.EmptyBytes)
	bfr.WriteString(expression)
	bb = bfr.Bytes()
	return IsTrue(bytes.Runes(bb), context)
}

func IsTrueBytes(expression []byte, context *xmlpath.Node) bool {
	return IsTrue(bytes.Runes(expression), context)
}

func IsTrue(expression []rune, context *xmlpath.Node) bool {
	var ok, truefalse bool
	var stack []booleanTest
	var exp *parsableExpression
	stack = make([]booleanTest, 0, 5)
	exp = newParsableExpression(expression)
	for {
		truefalse, ok = exp.parseBooleanExpression(context)
		if ok {
			stack = append(stack, buildBooleanTestType(truefalse))
			ok = exp.extractRunes(andRunes)
			if ok {
				stack = append(stack, buildBooleanTestTypeAnd())
			} else {
				ok = exp.extractRunes(orRunes)
				if ok {
					stack = append(stack, buildBooleanTestTypeOr())
				} else {
					// no more tests
					break
				}
			}
		} else {
			break
		}
	}
	if len(stack) == 0 {
		return false
	} else {
		truefalse = stack[0].truefalse
		length := len(stack)
		for i := 1; i < length; i++ {
			andor := stack[i].andor
			i++
			if i < length {
				ok = stack[i].truefalse
				if andor == andLogicalType {
					truefalse = truefalse && ok
				} else {
					truefalse = truefalse || ok
				}
			}
		}
		return truefalse
	}
}

func (this *parsableExpression) parseBooleanExpression(context *xmlpath.Node) (truefalse, ok bool) {
	this.skipWhiteSpace()
	var path []rune
	var stringValue, nodeString string
	var index, intValue int
	var ctype comparisonType
	var nodes []*xmlpath.Node
	// 3 simple valid booleans are:
	//   simple: count(p)
	//   simple: exists(p)
	//   simple: p = ""
	// 1 combination: ((p1)[1]/p2/(p3)[3] = "hello" and count((p3)[3]/p4) = 3)
	path, ctype, intValue, ok = this.extractCountTest()
	if ok {
		// count(p1) = 1 ?
		nodes = PathNodes(path, context)
		switch ctype {
		case comparisonTypeEqual:
			truefalse = intValue == len(nodes)
		case comparisonTypeNotEqual:
			truefalse = intValue != len(nodes)
		case comparisonTypeLessThan:
			truefalse = len(nodes) < intValue
		case comparisonTypeGreaterThan:
			truefalse = len(nodes) > intValue
		case comparisonTypeLessThanEqual:
			truefalse = len(nodes) <= intValue
		case comparisonTypeGreaterThanEqual:
			truefalse = len(nodes) >= intValue
		}
		return
	}
	// exists(p1)
	path, ok = this.extractExistsTest()
	if ok {
		nodes = PathNodes(path, context)
		truefalse = len(nodes) > 0
		return
	}
	// (p1)[1] = "hello"
	path, index, ok = this.extractParenIndexed()
	if ok {
		ctype, ok = this.extractComparitor()
		if ok {
			stringValue, ok = this.extractString()
			if ok {
				nodes = PathNodes(path, context)
				if len(nodes) >= index && index > 0 {
					stringValue = strings.Trim(stringValue, data.WhiteSpaceString)
					nodeString = nodes[index-1].String()
					nodeString = strings.Trim(nodeString, data.WhiteSpaceString)
					if ctype == comparisonTypeEqual {
						truefalse = stringValue == nodeString
					} else {
						truefalse = stringValue != nodeString
					}
				}
			}
		} else {
			// its not a comparison, its a path
			// ex: (p1)[1]/p2
		}
		return
	}
	// ((p1)[1]/p2/(p3)[3] = "hello" and count((p3)[3]/p4) = 3)
	path, ok = this.extractParenWrapped()
	if ok {
		truefalse = IsTrue(path, context)
	}
	return
}
